﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Security.Cryptography;
using System.Threading;

namespace RentIt06
{
    public class FileService : IFileService
    {
        private const string FILE_PATH = "C:\\RentItServices\\RentIt06\\";

        private struct ComputeHashState
        {
            public int mediaeId;
            public int uploadId;
            public byte[] hash;
            public Stream stream;
        }

        public void UploadMedia(MediaUploadMessage message) {
            if (message == null || message.FileStream == null)
                return;

            Log.Write("Uploading " + message.Token);

            int mediaId;
            byte[] hash;

            using (var map = new db.DataMapDataContext()) {
                var upload = map.Uploads.First(up => up.id == message.Token);

                // verify that the given upload token does exist
                if (upload == null) {
                    message.FileStream.Close();
                    return;
                }

                // verify that the token has not expired
                if (upload.expires < DateTime.Now) {
                    message.FileStream.Close();
                    map.Uploads.DeleteOnSubmit(upload);
                    map.SubmitChanges();
                    return;
                }

                mediaId = upload.media;
                // fetch hash for comparision
                hash = upload.hash.ToArray();

                // remove used token
                map.Uploads.DeleteOnSubmit(upload);
                map.SubmitChanges();
            }
            Log.Write("Create pipes");

            // create pipe for inter thread communication
            var toHash = new AnonymousPipeServerStream();
            var hashThreadStream = new AnonymousPipeClientStream(PipeDirection.In, toHash.ClientSafePipeHandle);

            Log.Write("Create pipes. Done");

            // que a new async task to verify the file while uploading
            ThreadPool.QueueUserWorkItem(
                UploadHashCompleted,
                new ComputeHashState
                    { mediaeId = mediaId, uploadId = message.Token, hash = hash, stream = hashThreadStream });

            Log.Write("Hash thread qued");


            long totalBytes = 0;

            // save the file to the hard drive to a temp file
            using (var file = new FileStream(FILE_PATH + mediaId + "." + message.Token + ".rentit.temp", FileMode.Append))
            {
                Log.Write("File opened");
                const int bufferLen = 65000;
                var buffer = new byte[bufferLen];
                int count;
                // read the file in chunks and write the chunks to hard drive and verification thread at the same time
                while ((count = message.FileStream.Read(buffer, 0, bufferLen)) > 0)
                {
                    totalBytes += count;
                    file.Write(buffer, 0, count);
                    toHash.Write(buffer, 0, count);
                }
                Log.Write("Read: " + totalBytes + " bytes");
            }
            // it is important that the file is closed before the pipe, otherwise the verification thread could
            // start operating on the temp file before this thread had released the file.
            message.FileStream.Close();
            Log.Write("Done uploading!");
            toHash.Close();
            // save filelength in db
            using (var map = new db.DataMapDataContext()) {
                var media = map.Medias.First(m => m.id == mediaId);
                media.bytes = totalBytes;
                map.SubmitChanges();
            }
        }

        public Stream DownloadMedia(int token, int id)
        {
            db.Download dl;
            using (var map = new db.DataMapDataContext())
            {
                Log.Write("get download token:" + token);
                var temp = map.Downloads.Where(up => up.id == token);
                Log.Write("get: " + temp);
                if (!temp.Any()) throw new FaultException(new FaultReason("No downloads with the given ID and token found"));
                dl = temp.First();
            }
            // verify the token is valid
            if (dl.media != id && dl.expires < DateTime.Now)
                throw new FaultException(new FaultReason("Token invalid or expired"));
            const string path = @"C:\RentItServices\RentIt06\";
            var downloadStream = new FileStream(path + id + ".rentit", FileMode.Open, FileAccess.Read);
            return downloadStream;
        }

        // upload verification thread
        private void UploadHashCompleted(Object obj)
        {
            var state = (ComputeHashState) obj;

            Log.Write("Hash thread started!");

            // calculate the hash from the pipe generated in UploadMedia
            var sha = SHA512.Create();
            var computed = sha.ComputeHash(state.stream);
            // note SHA512 buffers the entire file while calculating. For better memory optimazation we
            // should have used an alternative algorithm that does not buffer.

            Log.Write("Hash: " + BitConverter.ToString(computed).Replace("-", ""));

            // verify that the hashes matches
            if (state.hash.Where((t, i) => computed[i] != t).Any())
            {
                Log.Write("Hashes did not match");
                File.Delete(FILE_PATH + state.mediaeId + "t" + state.uploadId + ".rentit.temp");
                Log.Write("File deleted");
                return;
            }

            // if the hashes matches overwrite the existing file (if any) with the temp file
            // this is why it is crucial that the UploadMedia thread closes the file before closing the pipe.
            Log.Write("Hashes match!");
            File.Delete(FILE_PATH + state.mediaeId + ".rentit");
            File.Move(FILE_PATH + state.mediaeId + "." + state.uploadId + ".rentit.temp", FILE_PATH + state.mediaeId + ".rentit");
            Log.Write("File renamed to: " + FILE_PATH + state.mediaeId + ".rentit");
        }
    }
}
